//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Text;
using System.IO;
using EffiProz.Core.Store;
using EffiProz.Core.Lib;
using EffiProz.Core.RowIO;
using EffiProz.Core.ScriptIO;
using EffiProz.Core.Errors;
using EffiProz.Core.Lib.IO;

namespace EffiProz.Core.Persist
{
    // Ito Kazumitsu 20030328 - patch 1.7.2 - character encoding support
    // Dimitri Maziuk - patch for NL _in string support
    // sqlbob@users - updated for 1.8.0 to allow new-lines _in fields
    // fredt@users - updated for 1.8.0 to allow correct behaviour with transactions

    /**
     * Acts as a buffer manager for a single TEXT table with respect its RowAVL data.<p>
     *
     * Handles read/write operations on the table's text format data file using a
     * compatible pair of org.hsqldb.rowio input/output class instances.
     *
     *
     * @author sqlbob@users (RMP)
     * @version 1.8.0
     * @since 1.7.0
     */
    public class TextCache : DataFileCache
    {
        //state of Cache
        public const String NL = "\n";
        public String fs;
        public String vs;
        public String lvs;
        public String stringEncoding;
        public bool isQuoted;
        public bool isAllQuoted;
        public bool ignoreFirst;
        protected String header;
        public Table table;
        private ObjectCacheHashMap uncommittedCache;

        //
        const char DOUBLE_QUOTE_CHAR = '\"';
        const char BACKSLASH_CHAR = '\\';
        const char LF_CHAR = '\n';
        const char CR_CHAR = '\r';

        /**
         *  The source string for a cached table is evaluated and the parameters
         *  are used to open the source file.<p>
         *
         *  Settings are used in this order: (1) settings specified in the
         *  source string for the table (2) global database settings in
         *  *.properties file (3) program defaults
         *
         *  fredt - this used to write rows as soon as they are inserted
         *  but now this is subject to session autoCommit / or commit
         *  storeOnInsert = true;
         */
        public TextCache(Table table, String name)
            : base(table.database, name)
        {

            this.table = table;
            uncommittedCache = new ObjectCacheHashMap(5);
        }

        protected override void initParams(Database database, String baseFileName)
        {

            this.dataFileName = dataFileName;
            this.database = database;
            fa = FileUtil.GetDefaultInstance();

            EffiProzProperties tableprops =
                EffiProzProperties.delimitedArgPairsToProps(baseFileName, "=", ";",
                    "textdb");
            EffiProzDatabaseProperties dbProps = database.getProperties();

            //-- Get file name
            switch (tableprops.errorCodes.Length)
            {

                case 0:
                    throw Error.error(ErrorCode.X_S0501);
                case 1:

                    // source file name is the only key without a value
                    this.dataFileName = tableprops.errorKeys[0].Trim();
                    break;

                default:
                    throw Error.error(ErrorCode.X_S0502);
            }

            //-- Get separators: from database properties, then from table properties
            fs = dbProps.getStringProperty(EffiProzDatabaseProperties.textdb_fs);
            fs = tableprops.getProperty(EffiProzDatabaseProperties.textdb_fs, fs);
            vs = dbProps.getStringProperty(EffiProzDatabaseProperties.textdb_vs);
            vs = tableprops.getProperty(EffiProzDatabaseProperties.textdb_vs, vs);
            lvs = dbProps.getStringProperty(EffiProzDatabaseProperties.textdb_lvs);
            lvs = tableprops.getProperty(EffiProzDatabaseProperties.textdb_lvs, lvs);

            if (vs == null)
            {
                vs = fs;
            }

            if (lvs == null)
            {
                lvs = fs;
            }

            fs = translateSep(fs);
            vs = translateSep(vs);
            lvs = translateSep(lvs);

            if (fs.Length == 0 || vs.Length == 0 || lvs.Length == 0)
            {
                throw Error.error(ErrorCode.X_S0503);
            }

            //-- Get bools
            ignoreFirst =
                dbProps.isPropertyTrue(EffiProzDatabaseProperties.textdb_ignore_first);
            ignoreFirst = tableprops.isPropertyTrue(
                EffiProzDatabaseProperties.textdb_ignore_first, ignoreFirst);
            isQuoted =
                dbProps.isPropertyTrue(EffiProzDatabaseProperties.textdb_quoted);
            isQuoted =
                tableprops.isPropertyTrue(EffiProzDatabaseProperties.textdb_quoted,
                                          isQuoted);
            isAllQuoted =
                dbProps.isPropertyTrue(EffiProzDatabaseProperties.textdb_all_quoted);
            isAllQuoted =
                tableprops.isPropertyTrue(EffiProzDatabaseProperties.textdb_all_quoted,
                                          isAllQuoted);
            stringEncoding =
                dbProps.getStringProperty(EffiProzDatabaseProperties.textdb_encoding);
            stringEncoding =
                tableprops.getProperty(EffiProzDatabaseProperties.textdb_encoding,
                                       stringEncoding);

            //-- Get size and scale
            int cacheScale = tableprops.getIntegerProperty(
                EffiProzDatabaseProperties.textdb_cache_scale,
                dbProps.getIntegerProperty(
                    EffiProzDatabaseProperties.textdb_cache_scale));
            int cacheSizeScale = tableprops.getIntegerProperty(
                EffiProzDatabaseProperties.textdb_cache_size_scale,
                dbProps.getIntegerProperty(
                    EffiProzDatabaseProperties.textdb_cache_size_scale));
            int lookupTableLength = 1 << cacheScale;
            int avgRowBytes = 1 << cacheSizeScale;

            maxCacheRows = lookupTableLength * 3;
            maxCacheBytes = maxCacheRows * avgRowBytes;
            maxDataFileSize = int.MaxValue;
            cachedRowPadding = 1;
            cacheFileScale = 1;
        }

        protected override void initBuffers()
        {

            if (isQuoted || isAllQuoted)
            {
                rowIn = new RowInputTextQuoted(fs, vs, lvs, isAllQuoted);
                rowOut = new RowOutputTextQuoted(fs, vs, lvs, isAllQuoted,
                                                 stringEncoding);
            }
            else
            {
                rowIn = new RowInputText(fs, vs, lvs, false);
                rowOut = new RowOutputText(fs, vs, lvs, false, stringEncoding);
            }
        }

        private static String translateSep(String sep)
        {
            return translateSep(sep, false);
        }

        /**
         * Translates the escaped characters in a separator string and returns
         * the non-escaped string.
         */
        private static String translateSep(String sep, bool isProperty)
        {

            if (sep == null)
            {
                return null;
            }

            int next = sep.IndexOf(BACKSLASH_CHAR);

            if (next != -1)
            {
                int start = 0;
                char[] sepArray = sep.ToCharArray();
                char ch = (char)0;
                int len = sep.Length;
                StringBuilder sb = new StringBuilder(len);

                do
                {
                    sb.Append(sepArray, start, next - start);

                    start = ++next;

                    if (next >= len)
                    {
                        sb.Append(BACKSLASH_CHAR);

                        break;
                    }

                    if (!isProperty)
                    {
                        ch = sepArray[next];
                    }

                    if (ch == 'n')
                    {
                        sb.Append(LF_CHAR);

                        start++;
                    }
                    else if (ch == 'r')
                    {
                        sb.Append(CR_CHAR);

                        start++;
                    }
                    else if (ch == 't')
                    {
                        sb.Append('\t');

                        start++;
                    }
                    else if (ch == BACKSLASH_CHAR)
                    {
                        sb.Append(BACKSLASH_CHAR);

                        start++;
                    }
                    else if (ch == 'u')
                    {
                        start++;

                        sb.Append(
                            (char)int.Parse(
                                sep.Substring(start, 4), System.Globalization.NumberStyles.HexNumber));

                        start += 4;
                    }
                    else if (sep.Substring(next).StartsWith("semi"))
                    {
                        sb.Append(';');

                        start += 4;
                    }
                    else if (sep.Substring(next).StartsWith("space"))
                    {
                        sb.Append(' ');

                        start += 5;
                    }
                    else if (sep.Substring(next).StartsWith("quote"))
                    {
                        sb.Append(DOUBLE_QUOTE_CHAR);

                        start += 5;
                    }
                    else if (sep.Substring(next).StartsWith("apos"))
                    {
                        sb.Append('\'');

                        start += 4;
                    }
                    else
                    {
                        sb.Append(BACKSLASH_CHAR);
                        sb.Append(sepArray[next]);

                        start++;
                    }
                } while ((next = sep.IndexOf(BACKSLASH_CHAR, start)) != -1);

                sb.Append(sepArray, start, len - start);

                sep = sb.ToString();
            }

            return sep;
        }

        /**
         *  Opens a data source file.
         */
        public override void open(bool rdy)
        {

            fileFreePosition = 0;

            try
            {
                int type = ScaledRAFile.DATA_FILE_RAF;

                dataFile = ScaledRAFile.newScaledRAFile(database, dataFileName,
                        rdy, type);
                fileFreePosition = dataFile.length();

                if (fileFreePosition > int.MaxValue)
                {
                    throw Error.error(ErrorCode.DATA_FILE_IS_FULL);
                }

                initBuffers();
            }
            catch (Exception t)
            {
                throw Error.error(t, ErrorCode.FILE_IO_ERROR,
                                  ErrorCode.M_TextCache_openning_file_error,
                                  new Object[] {
                t.Message, dataFileName
            });
            }

            cacheReadonly = rdy;
        }

        public void reopen()
        {
            open(cacheReadonly);
        }

        /**
         *  Writes newly created rows to disk. In the current implentation,
         *  such rows have already been saved, so this method just removes a
         *  source file that has no rows.
         */
        public override void close(bool write)
        {
            lock (this)
            {
                if (dataFile == null)
                {
                    return;
                }

                try
                {
                    cache.saveAll();

                    bool empty = (dataFile.length() <= NL.Length);

                    dataFile.close();

                    dataFile = null;

                    if (empty && !cacheReadonly)
                    {
                        FileUtil.GetDefaultInstance().removeElement(dataFileName);
                    }
                }
                catch (Exception t)
                {
                    throw Error.error(t, ErrorCode.FILE_IO_ERROR,
                                      ErrorCode.M_TextCache_closing_file_error,
                                      new Object[] {
                t.Message, dataFileName
            });
                }
            }
        }

        /**
         * Closes the source file and deletes it if it is not read-only.
         */
        public void purge()
        {

            uncommittedCache.clear();

            try
            {
                if (cacheReadonly)
                {
                    close(false);
                }
                else
                {
                    if (dataFile != null)
                    {
                        dataFile.close();

                        dataFile = null;
                    }

                    FileUtil.GetDefaultInstance().removeElement(dataFileName);
                }
            }
            catch (Exception t)
            {
                throw Error.error(null, ErrorCode.FILE_IO_ERROR,
                                  ErrorCode.M_TextCache_purging_file_error,
                                  new Object[] {
                t.Message, dataFileName
            });
            }
        }

        /**
         *
         */
        public override void remove(int pos, PersistentStore store)
        {
            lock (this)
            {
                CachedObject row = (CachedObject)uncommittedCache.remove(pos);

                if (row != null)
                {
                    return;
                }

                row = cache.release(pos);

                clearRowImage(row);
            }

            //        release(pos);
        }

        public override void removePersistence(int pos)
        {
            lock (this)
            {
                CachedObject row = (CachedObject)uncommittedCache.get(pos);

                if (row != null)
                {
                    return;
                }

                row = cache.get(pos);

                clearRowImage(row);
            }
        }

        private void clearRowImage(CachedObject row)
        {

            try
            {
                int length = row.getStorageSize()
                             - ScriptWriterText.BYTES_LINE_SEP.Length;

                rowOut.reset();

                ByteArrayOutputStream outs = rowOut.getOutputStream();

                outs.fill(' ', length);
                outs.write(ScriptWriterText.BYTES_LINE_SEP);
                dataFile.seek(row.getPos());
                dataFile.write(outs.getBuffer(), 0, outs.size());
            }
            catch (IOException e)
            {
                throw Error.runtimeError(ErrorCode.U_S0500, e.Message);
            }
        }

        protected override RowInputInterface readObject(int pos)
        {
            lock (this)
            {
                try
                {
                    ByteArray buffer = new ByteArray(this, 80);
                    bool complete = false;
                    bool wasCR = false;
                    int c;
                    bool hasQuote = false;
                    bool wasNormal = false;

                    pos = findNextUsedLinePos(pos);

                    if (pos == -1)
                    {
                        return null;
                    }

                    dataFile.seek(pos);

                    while (!complete)
                    {
                        wasNormal = false;
                        c = dataFile.read();

                        if (c == -1)
                        {
                            if (buffer.length() == 0)
                            {
                                return null;
                            }

                            complete = true;

                            if (wasCR)
                            {
                                break;
                            }

                            if (!cacheReadonly)
                            {
                                dataFile.write(ScriptWriterText.BYTES_LINE_SEP, 0,
                                               ScriptWriterText.BYTES_LINE_SEP.Length);
                            }

                            break;
                        }

                        switch (c)
                        {

                            case DOUBLE_QUOTE_CHAR:
                                wasNormal = true;
                                complete = wasCR;
                                wasCR = false;

                                if (isQuoted)
                                {
                                    hasQuote = !hasQuote;
                                }
                                break;

                            case CR_CHAR:
                                wasCR = !hasQuote;
                                break;

                            case LF_CHAR:
                                complete = !hasQuote;
                                break;

                            default:
                                wasNormal = true;
                                complete = wasCR;
                                wasCR = false;
                                break;
                        }

                        buffer.append(c);
                    }

                    if (complete)
                    {
                        int length = (int)dataFile.getFilePointer() - pos;

                        if (wasNormal)
                        {
                            length--;
                        }

                        ((RowInputText)rowIn).setSource(buffer.ToString(), pos,
                                                         length);

                        return rowIn;
                    }

                    return null;
                }
                catch (IOException e)
                {
                    throw Error.error(ErrorCode.TEXT_FILE_IO, e);
                }
            }
        }

        public int readHeaderLine()
        {

            bool complete = false;
            bool wasCR = false;
            bool wasNormal = false;
            ByteArray buffer = new ByteArray(this, 80);

            while (!complete)
            {
                wasNormal = false;

                int c;

                try
                {
                    c = dataFile.read();

                    if (c == -1)
                    {
                        if (buffer.length() == 0)
                        {
                            return 0;
                        }

                        complete = true;

                        if (!cacheReadonly)
                        {
                            dataFile.write(ScriptWriterText.BYTES_LINE_SEP, 0,
                                           ScriptWriterText.BYTES_LINE_SEP.Length);
                        }

                        break;
                    }
                }
                catch (IOException )
                {
                    throw Error.error(ErrorCode.TEXT_FILE);
                }

                switch (c)
                {

                    case CR_CHAR:
                        wasCR = true;
                        break;

                    case LF_CHAR:
                        complete = true;
                        break;

                    default:
                        wasNormal = true;
                        complete = wasCR;
                        wasCR = false;
                        break;
                }

                if (wasCR || complete)
                {
                    continue;
                }

                buffer.append(c);
            }

            header = buffer.ToString();

            try
            {
                int length = (int)dataFile.getFilePointer();

                if (wasNormal)
                {
                    length--;
                }

                return length;
            }
            catch (IOException)
            {
                throw Error.error(ErrorCode.TEXT_FILE);
            }
        }

        // fredt - new method

        /**
         * Searches from file pointer, pos, and finds the beginning of the first
         * line that contains any non-space character. Increments the row counter
         * when a blank line is skipped.
         *
         * If none found return -1
         */
        int findNextUsedLinePos(int pos)
        {

            try
            {
                int firstPos = pos;
                int currentPos = pos;
                bool wasCR = false;

                dataFile.seek(pos);

                while (true)
                {
                    int c = dataFile.read();

                    currentPos++;

                    switch (c)
                    {

                        case CR_CHAR:
                            wasCR = true;
                            break;

                        case LF_CHAR:
                            wasCR = false;

                            ((RowInputText)rowIn).skippedLine();

                            firstPos = currentPos;
                            break;

                        case ' ':
                            if (wasCR)
                            {
                                wasCR = false;

                                ((RowInputText)rowIn).skippedLine();
                            }
                            break;

                        case -1:
                            return -1;

                        default:
                            return firstPos;
                    }
                }
            }
            catch (IOException e)
            {
                throw Error.error(ErrorCode.TEXT_FILE_IO, e);
            }
        }

        public override void add(CachedObject obj)
        {
            lock (this)
            {
                base.add(obj);
                clearRowImage(obj);
            }
        }

        public override CachedObject get(int i, PersistentStore store,
                                             bool keep)
        {
            lock (this)
            {

                if (i < 0)
                {
                    return null;
                }

                CachedObject o = (CachedObject)uncommittedCache.get(i);

                if (o == null)
                {
                    o = base.get(i, store, keep);
                }

                /*
                        if (o == null) {
                            o = base.get(i, store, keep);
                        }
                */
                return o;
            }
        }

        /**
         * This is called internally when old rows need to be removed from the
         * cache. Text table rows that have not been saved are those that have not
         * been committed yet. So we don't save them but add them to the
         * uncommitted cache until such time that they are committed or rolled
         * back- fredt
         */
        public override void saveRows(CachedObject[] rows, int offset,
                                             int count)
        {
            lock (this)
            {
                if (count == 0)
                {
                    return;
                }

                for (int i = offset; i < offset + count; i++)
                {
                    CachedObject r = rows[i];

                    uncommittedCache.put(r.getPos(), r);

                    rows[i] = null;
                }
            }
        }

        /**
         * In case the row has been moved to the uncommittedCache, removes it.
         * Then saves the row as normal.
         */
        public override void saveRow(CachedObject row)
        {
            lock (this)
            {
                uncommittedCache.remove(row.getPos());
                base.saveRow(row);
            }
        }

        public String getHeader()
        {
            return header;
        }

        public void setHeader(String header)
        {

            if (ignoreFirst && fileFreePosition == 0)
            {
                try
                {
                    writeHeader(header);

                    this.header = header;
                }
                catch (CoreException e)
                {
                    throw new CoreException(
                        e, Error.getMessage(ErrorCode.GENERAL_IO_ERROR),
                        ErrorCode.GENERAL_IO_ERROR);
                }

                return;
            }

            throw Error.error(ErrorCode.TEXT_TABLE_HEADER);
        }

        private void writeHeader(String header)
        {

            try
            {
                byte[] buf = null;
                String firstLine = header + NL;

                try
                {
                    buf = Encoding.GetEncoding(stringEncoding).GetBytes(firstLine);
                }
                catch (ArgumentException)
                {
                    buf = Encoding.UTF8.GetBytes(firstLine);
                }

                dataFile.write(buf, 0, buf.Length);

                fileFreePosition = buf.Length;
            }
            catch (IOException e)
            {
                throw Error.error(ErrorCode.TEXT_FILE_IO, e);
            }
        }

        private class ByteArray
        {

            private byte[] buffer;
            private int buflen;
            TextCache _o;

            public ByteArray(TextCache _o, int n)
            {
                buffer = new byte[n];
                buflen = 0;
                this._o = _o;
            }

            public void append(int c)
            {

                if (buflen >= buffer.Length)
                {
                    byte[] newbuf = new byte[buflen + 80];

                    Array.Copy(buffer, 0, newbuf, 0, buflen);

                    buffer = newbuf;
                }

                buffer[buflen] = (byte)c;

                buflen++;
            }

            public int length()
            {
                return buflen;
            }

            public void setLength(int l)
            {
                buflen = l;
            }

            public override String ToString()
            {

                try
                {
                    return Encoding.GetEncoding(_o.stringEncoding).GetString(buffer, 0, buflen);
                }
                catch (ArgumentException )
                {
                    return Encoding.UTF8.GetString(buffer, 0, buflen);
                }
            }
        }

        public int getLineNumber()
        {
            return ((RowInputText)rowIn).getLineNumber();
        }

        protected override void setFileModified()
        {
            fileModified = true;
        }

    }
}
